Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
4272aa6
feat(alerts): anomalies page through alert rules
Makisuo Nov 6, 2026
816b9ce
Merge remote-tracking branch 'origin/main' into feat/alerts-unified-i…
Makisuo Nov 6, 2026
b518097
feat(alerts): silences match an anomaly incident's service and enviro…
Makisuo Nov 6, 2026
dd25362
feat(web): anomalies live in the alerts hub
Makisuo Nov 6, 2026
d32578e
fix(alerts): review fixes for anomaly rules
Makisuo Nov 6, 2026
ba6f119
style(alerts): format review fixes
Makisuo Nov 6, 2026
67a2f17
Merge branch 'feat/alerts-unified-incidents' into feat/alerts-inciden…
Makisuo Nov 6, 2026
014359f
Merge branch 'feat/alerts-incident-labels' into feat/alerts-unified-ui
Makisuo Nov 6, 2026
74144e9
feat(alerts): maintenance windows
Makisuo Nov 6, 2026
5cd792c
fix(alerts): only Maple creates the default anomaly rule
Makisuo Nov 6, 2026
806ed4a
Merge branch 'feat/alerts-incident-labels' into feat/alerts-unified-ui
Makisuo Nov 6, 2026
8085b14
Merge branch 'feat/alerts-unified-incidents' into feat/alerts-inciden…
Makisuo Nov 6, 2026
5db38bb
Merge branch 'feat/alerts-unified-ui' into feat/alerts-maintenance-wi…
Makisuo Nov 6, 2026
30aab29
fix(web): anomaly editor fixes from the preview test
Makisuo Nov 6, 2026
1351130
Merge remote-tracking branch 'origin/main' into feat/alerts-unified-i…
Makisuo Nov 6, 2026
ce36af7
Merge branch 'feat/alerts-unified-incidents' into feat/alerts-inciden…
Makisuo Nov 6, 2026
2b42ce3
Merge branch 'feat/alerts-incident-labels' into feat/alerts-unified-ui
Makisuo Nov 6, 2026
69b1cec
Merge branch 'feat/alerts-unified-ui' into feat/alerts-maintenance-wi…
Makisuo Nov 6, 2026
5dac832
fix(alerts): maintenance window sync and baseline fixes from review
Makisuo Nov 6, 2026
d4fd896
test(alerts): scope the anomaly silences test rule now that only Mapl…
Makisuo Nov 6, 2026
a1f94c9
Merge branch 'feat/alerts-incident-labels' into feat/alerts-unified-ui
Makisuo Nov 6, 2026
bf47132
Merge branch 'feat/alerts-unified-ui' into feat/alerts-maintenance-wi…
Makisuo Nov 6, 2026
0901897
docs(domain): keep the hold reason comment on its schema
Makisuo Nov 6, 2026
cf004c9
Merge branch 'feat/alerts-incident-labels' into feat/alerts-unified-ui
Makisuo Nov 6, 2026
9f9069c
Merge branch 'feat/alerts-unified-ui' into feat/alerts-maintenance-wi…
Makisuo Nov 6, 2026
720e8cf
fix(alerts): templates show an anomaly's severity as a word
Makisuo Nov 6, 2026
1d1e149
Merge branch 'feat/alerts-unified-incidents' into feat/alerts-inciden…
Makisuo Nov 6, 2026
0ec32a1
Merge branch 'feat/alerts-incident-labels' into feat/alerts-unified-ui
Makisuo Nov 6, 2026
ff878b4
Merge branch 'feat/alerts-unified-ui' into feat/alerts-maintenance-wi…
Makisuo Nov 6, 2026
62a23d0
fix(web): the anomalies open view reads open incidents only
Makisuo Nov 6, 2026
f998895
Merge branch 'feat/alerts-unified-ui' into feat/alerts-maintenance-wi…
Makisuo Nov 6, 2026
c62cebf
fix(alerts): the default anomaly rule message names actions that exist
Makisuo Nov 6, 2026
e6381ad
Merge branch 'feat/alerts-unified-incidents' into feat/alerts-inciden…
Makisuo Nov 6, 2026
7534186
Merge branch 'feat/alerts-incident-labels' into feat/alerts-unified-ui
Makisuo Nov 6, 2026
2ac84a9
Merge branch 'feat/alerts-unified-ui' into feat/alerts-maintenance-wi…
Makisuo Nov 6, 2026
68df751
fix(alerts): anomaly rule test runs and MCP text
Makisuo Nov 6, 2026
6b79565
fix(alerts): default rule message says scoped rules still page
Makisuo Nov 6, 2026
54e709a
Merge branch 'feat/alerts-unified-incidents' into feat/alerts-inciden…
Makisuo Nov 6, 2026
1acf1cb
Merge branch 'feat/alerts-incident-labels' into feat/alerts-unified-ui
Makisuo Nov 6, 2026
395c4c4
fix(web): anomaly view counts and copy follow the service filter; no …
Makisuo Nov 6, 2026
9201f7a
Merge branch 'feat/alerts-unified-ui' into feat/alerts-maintenance-wi…
Makisuo Nov 6, 2026
adb588e
Merge remote-tracking branch 'origin/main' into feat/alerts-unified-i…
Makisuo Nov 6, 2026
48582b6
chore(db): merge the anomaly rules and escalation lease migrations
Makisuo Nov 6, 2026
b362bef
Merge branch 'feat/alerts-unified-incidents' into feat/alerts-inciden…
Makisuo Nov 6, 2026
e40ff81
chore(db): merge the incident labels migration with main
Makisuo Nov 6, 2026
93c6655
Merge branch 'feat/alerts-incident-labels' into feat/alerts-unified-ui
Makisuo Nov 6, 2026
38b9f7b
Merge branch 'feat/alerts-unified-ui' into feat/alerts-maintenance-wi…
Makisuo Nov 6, 2026
51a8c9b
chore(db): merge the maintenance windows migration with main
Makisuo Nov 6, 2026
6ada386
fix(web): the default anomaly rule's empty routing is not a warning
Makisuo Nov 6, 2026
c022456
fix(mcp): maintenance window tools never pre-confirm a delete; baseli…
Makisuo Nov 6, 2026
a249a22
Merge branch 'feat/alerts-unified-ui' into feat/alerts-maintenance-wi…
Makisuo Nov 6, 2026
bd86ddc
fix(alerts): anomaly rules record that they ran
Makisuo Nov 6, 2026
6204940
fix(alerts): unrouted anomaly tiers never push; disabling detection r…
Makisuo Nov 6, 2026
97db19f
Merge remote-tracking branch 'origin/feat/alerts-unified-incidents' i…
Makisuo Nov 6, 2026
e4f9529
Merge branch 'feat/alerts-incident-labels' into feat/alerts-unified-ui
Makisuo Nov 6, 2026
9ae96ab
fix(web): anomaly rule page and editor fixes from the preview test
Makisuo Nov 6, 2026
012761d
fix(alerts): maintenance window schedule error names no internal field
Makisuo Nov 6, 2026
0b8502e
Merge remote-tracking branch 'origin/feat/alerts-unified-ui' into fea…
Makisuo Nov 6, 2026
8a1707e
test(web): the unrouted anomaly test removes a destination the rule had
Makisuo Nov 6, 2026
99835dc
fix(alerts): refuse re-enabling an ended one-off window; name a delet…
Makisuo Nov 6, 2026
89665ce
fix(web): a maintenance window's silence shows its window instead of …
Makisuo Nov 6, 2026
b7a4eca
Merge remote-tracking branch 'origin/feat/alerts-unified-ui' into fea…
Makisuo Nov 6, 2026
f65a0f7
Merge origin/main into feat/alerts-maintenance-windows
Makisuo Nov 6, 2026
1a16bad
Merge remote-tracking branch 'origin/main' into feat/alerts-maintenan…
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
2 changes: 2 additions & 0 deletions apps/ai/src/mcp/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AlertIncidentAckService } from "@maple/backend/services/alerts/AlertInc
import { AlertIncidentPagesService } from "@maple/backend/services/alerts/AlertIncidentPagesService"
import { AlertRulesService } from "@maple/backend/services/alerts/AlertRulesService"
import { EscalationChainsService } from "@maple/backend/services/alerts/EscalationChainsService"
import { AlertMaintenanceWindowsService } from "@maple/backend/services/alerts/AlertMaintenanceWindowsService"
import { OnCallSchedulesService } from "@maple/backend/services/alerts/OnCallSchedulesService"
import { AlertSilencesService } from "@maple/backend/services/alerts/AlertSilencesService"

Expand Down Expand Up @@ -279,6 +280,7 @@ const McpRuntimeServicesLive = Layer.mergeAll(
AlertIncidentPagesService.layer,
AlertRulesService.layer,
EscalationChainsService.layer,
AlertMaintenanceWindowsService.layer,
OnCallSchedulesService.layer,
AlertSilencesService.layer,
AlertsService.layer,
Expand Down
3 changes: 3 additions & 0 deletions apps/ai/src/mcp/lib/alert-silences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ export const createSilenceErrors = {
export const endSilenceErrors = {
"@maple/http/errors/AlertSilenceNotFoundError": (error: AlertSilenceNotFoundError) =>
Effect.fail(silenceNotFound(error.silenceId)),
// A maintenance window's silence is ended through the window.
"@maple/http/errors/AlertValidationError": (error: AlertValidationError) =>
Effect.fail(new McpInvalidInputError({ message: error.message, parameter: "silence_id" })),
"@maple/http/errors/AlertForbiddenError": failWith("end_alert_silence"),
"@maple/http/errors/AlertPersistenceError": failWith("end_alert_silence"),
}
133 changes: 133 additions & 0 deletions apps/ai/src/mcp/lib/maintenance-windows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Array as Arr, Effect } from "effect"
import type {
AlertForbiddenError,
AlertMaintenanceWindowDocument,
AlertMaintenanceWindowNotFoundError,
AlertPersistenceError,
AlertValidationError,
} from "@maple/domain/http"
import type { MaintenanceWindowRow } from "@maple/domain/mcp-outputs"
import { McpInvalidInputError } from "../tools/types"
import { toMcpHttpError } from "./map-http-error"
import { doc } from "./tool-doc"

type WindowRow = typeof MaintenanceWindowRow.Type

export const MAINTENANCE_WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const

const listOf = <K extends string>(key: K, values: ReadonlyArray<string> | undefined) =>
values === undefined ? undefined : { [key]: [...values] }

export const toMaintenanceWindowRow = (window: AlertMaintenanceWindowDocument): WindowRow => ({
id: window.id,
name: window.name,
reason: window.reason,
matchers: {
...listOf("ruleIds", window.matchers.ruleIds),
...listOf("tags", window.matchers.tags),
...listOf("services", window.matchers.services),
...listOf("environments", window.matchers.environments),
},
schedule:
window.schedule._tag === "once"
? { type: "once", startsAt: window.schedule.startsAt, endsAt: window.schedule.endsAt }
: {
type: "weekly",
timeZone: window.schedule.timeZone,
days: [...window.schedule.days],
startTime: window.schedule.startTime,
durationMinutes: window.schedule.durationMinutes,
},
excludeFromBaselines: window.excludeFromBaselines,
enabled: window.enabled,
nextOccurrence: window.nextOccurrence,
createdBy: window.createdBy,
})

/** `weekly sat,sun 02:00 Europe/Berlin for 120m`, or the one-off window's bounds. */
export const formatSchedule = (schedule: WindowRow["schedule"]): string =>
schedule.type === "once"
? `once ${schedule.startsAt ?? "?"} to ${schedule.endsAt ?? "?"}`
: `weekly ${Arr.join(schedule.days ?? [], ",")} ${schedule.startTime ?? "?"} ${schedule.timeZone ?? "?"} for ${schedule.durationMinutes ?? "?"}m`

/** The matchers as one line, e.g. `services: checkout AND environments: production`. */
export const formatWindowMatchers = (matchers: WindowRow["matchers"]): string => {
const entries: ReadonlyArray<readonly [string, ReadonlyArray<string> | undefined]> = [
["rules", matchers.ruleIds],
["tags", matchers.tags],
["services", matchers.services],
["environments", matchers.environments],
]
const parts = Arr.flatMap(entries, ([label, values]) =>
values === undefined || values.length === 0 ? [] : [`${label}: ${Arr.join(values, ", ")}`],
)
return parts.length === 0 ? "-" : Arr.join(parts, " AND ")
}

const formatNext = (window: WindowRow) =>
window.nextOccurrence === null
? "-"
: `${window.nextOccurrence.startsAt} to ${window.nextOccurrence.endsAt}`

/** Only service and environment matchers keep hours out of anomaly baselines. */
const baselineExclusion = (window: WindowRow): string => {
if (!window.excludeFromBaselines) return "no"
const scoped =
(window.matchers.services?.length ?? 0) > 0 || (window.matchers.environments?.length ?? 0) > 0
return scoped ? "yes" : "no (only service or environment matchers affect baselines)"
}

export const maintenanceWindowFields = (window: WindowRow) =>
doc.fields([
["ID", window.id],
["Name", window.name],
["Reason", window.reason ?? "-"],
["Schedule", formatSchedule(window.schedule)],
["Matches", formatWindowMatchers(window.matchers)],
["Next occurrence", formatNext(window)],
["Enabled", window.enabled ? "yes" : "no"],
["Excluded from baselines", baselineExclusion(window)],
])

export const maintenanceWindowTableRow = (window: WindowRow) => [
window.id,
window.name,
formatSchedule(window.schedule),
formatWindowMatchers(window.matchers),
formatNext(window),
window.enabled ? "yes" : "no",
]

const failWith = (pipeName: string) => (error: AlertForbiddenError | AlertPersistenceError) =>
Effect.fail(toMcpHttpError(pipeName)(error))

/** create_maintenance_window failures: an invalid schedule or matcher is the caller's to fix. */
export const createWindowErrors = {
"@maple/http/errors/AlertValidationError": (error: AlertValidationError) =>
Effect.fail(
new McpInvalidInputError({
message: Arr.join(
[
`Invalid maintenance window: ${error.message}`,
...Arr.filter(error.details, (detail) => detail !== error.message),
],
". ",
),
}),
),
"@maple/http/errors/AlertForbiddenError": failWith("create_maintenance_window"),
"@maple/http/errors/AlertPersistenceError": failWith("create_maintenance_window"),
}

/** delete_maintenance_window failures: an unknown id is invalid input on maintenance_window_id. */
export const deleteWindowErrors = {
"@maple/http/errors/AlertMaintenanceWindowNotFoundError": (error: AlertMaintenanceWindowNotFoundError) =>
Effect.fail(
new McpInvalidInputError({
message: `Maintenance window not found: ${error.maintenanceWindowId}. Use list_maintenance_windows to find window IDs.`,
parameter: "maintenance_window_id",
}),
),
"@maple/http/errors/AlertForbiddenError": failWith("delete_maintenance_window"),
"@maple/http/errors/AlertPersistenceError": failWith("delete_maintenance_window"),
}
6 changes: 6 additions & 0 deletions apps/ai/src/mcp/lib/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { decodePublicId, PublicIdPrefixes } from "@maple/domain/http/v2"
import {
AlertDestinationId,
AlertIncidentId,
AlertMaintenanceWindowId,
AlertRuleId,
AlertSilenceId,
DashboardId,
Expand Down Expand Up @@ -166,6 +167,11 @@ const RESOURCE_IDS = {
schema: EscalationChainId,
noun: "escalation chain",
},
alertMaintenanceWindow: {
prefix: PublicIdPrefixes.alertMaintenanceWindow,
schema: AlertMaintenanceWindowId,
noun: "maintenance window",
},
oncallSchedule: {
prefix: PublicIdPrefixes.oncallSchedule,
schema: OnCallScheduleId,
Expand Down
1 change: 1 addition & 0 deletions apps/ai/src/mcp/resources/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ first occurrences ──> (candidate) ──> issue in \`triage\`
- Deploy verification: service_deployments (versions per service, newest vs previous) -> compare_periods around the rollout. Route usage over up to 90 days: route_usage. Silence check before calling an outage: ingest_freshness. Volume per service: ingest_usage. Database query shapes by volume: db_query_volume
- Alert management: list_alert_rules / list_alert_destinations -> get_alert_rule -> preview_alert_rule (dry-run before saving; a window with no data is a blind rule, not a healthy one) -> create_alert_rule / update_alert_rule / delete_alert_rule -> list_alert_incidents
- Paging step by step until someone acknowledges: list_escalation_chains / create_escalation_chain (steps of destinations + wait_minutes) / update_escalation_chain / delete_escalation_chain; attach to a rule with create_alert_rule / update_alert_rule escalation_chain_id. The rule's own destinations still fire at once.
- Planned quiet periods (a nightly job, a weekly maintenance slot): create_maintenance_window (once or weekly, with matchers) / list_maintenance_windows / delete_maintenance_window. Each occurrence silences notifications while it runs and, for services or environments, keeps its hours out of anomaly baselines.
- Who is on call when: list_oncall_schedules / who_is_on_call (schedule id or name, optional at) / create_oncall_schedule (layers of workspace members rotating daily, weekly or every N hours) / update_oncall_schedule / delete_oncall_schedule; cover a window with create_oncall_override / delete_oncall_override.
- Quieting alerts for a window (maintenance, a known outage): create_alert_silence (matchers + duration_minutes or ends_at) / list_alert_silences / end_alert_silence. Incidents still open; only notifications are held back.
- Uptime: list_uptime_monitors is every http/tcp/dns check and heartbeat with its status and last check; a failed check's trace id goes to inspect_trace. Admins manage them with create_uptime_monitor / update_uptime_monitor (enabled=false pauses) / delete_uptime_monitor. A down monitor opens an alert incident with signal type \`uptime\`; its rule is managed only through the monitor.
Expand Down
Loading
Loading