Skip to content

feat(web): anomalies live in the alerts hub - #2320

Merged
Makisuo merged 51 commits into
mainfrom
feat/alerts-unified-ui
Nov 6, 2026
Merged

Makisuo merged 51 commits into
mainfrom
feat/alerts-unified-ui

fix(web): an anomaly investigation never sends readings from evidence…

5301042
Select commit
Loading
Failed to load commit list.
Maple Review Bot / Maple / review completed Nov 6, 2026 in 5m 54s

Confidence 6/10 · 2 issues to address

🟡 Confidence 6/10 · needs attention. Two untested correctness gaps on the new anomaly editor paths; the incident, anomalies-detail and lab files went unread as the pass ran out of time.
2 warnings −2 · 1 note · tests partial −1 · risk medium · 0/1 new units observable −1

Moves anomalies into the Alerts hub: an Anomaly rule mode, an anomaly evidence incident page, and /anomalies redirecting to the hub. Two correctness gaps remain in the new anomaly branches. The McpQueryError issues in page/registry.ts are unrelated MCP tool failures the diff does not touch. Unread: incident page, anomalies-detail, kit and lab files.

  • The Inbox gains an Anomalies view driven by signal and anomalyStatus
  • Rule editor gains an Anomaly mode with scope and per-tier routing
  • Anomaly incidents draw the detector's evidence instead of the rule chart
  • /anomalies redirects to /alerts?signal=anomaly; the list page is removed

Findings

🟠 Warning · F12 · An anomaly rule routed by an escalation chain cannot be saved

correctness · apps/web/src/page/alerts/editor/derive.ts:136-138

anomalyIssues counts only destinationIds and escalation.destinationIds, while isNotifyMissingOf (editor/view.ts:112) also counts escalationChainId and escalation.escalationChainId as routing. A scoped anomaly rule whose tier starts a chain shows "Who gets notified" as done but saveIssues adds "Who gets notified", so the action bar prints "Fix before saving" and Save stays disabled.

Treat a chain as routing here too: add `form.escalationChainId === null` and `(form.escalation?.escalationChainId ?? null) === null` to the destination condition, mirroring `isNotifyMissingOf`.
🟠 Warning · F13 · Leaving Anomaly mode for SQL skips carryThreshold, saving a threshold 100× too high

correctness · packages/web-data/src/alerts/rule-modes.ts:162-163

leaveAnomaly restores the Built-in error-rate default threshold (a percent form value), and the SQL branch returns it verbatim, skipping the carryThreshold the Built-in → SQL branch at line 165 applies. formThresholdToDomain converts percent only for percent signals, so raw_query stores the raw number: switching an anomaly rule to Query mode saves 5 where an equivalent SQL rule saves 0.05.

	if (current === "anomaly") {
		const left = leaveAnomaly(form)
		if (next === "builtin") return { form: left, notice: null }
		const moved = moveToKind(left, next)
		return { form: carryThreshold(left, moved.form), notice: moved.notice }
	}

Still open from earlier reviews

Fixed since the last review

  • ✅ F10 · A just-resolved anomaly reads "now ago" in the resolved list
  • ✅ F11 · Test named for carrying both tiers' destinations asserts neither

Production impact

Open errors in the changed files (2)
Issue Service Occurrences File
@maple/mcp/errors/McpQueryError (5 issues) maple-chat 33 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError (5 issues) maple-ai 15 apps/web/src/page/registry.ts

After this merges, Maple checks whether they stop.

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/web/src/page/alerts/incident/view.ts 26 preview 26/day, 8.9% err
apps/web/src/page/alerts/rule/view-overview.ts 26 preview 26/day, 8.9% err
What was checked
  • Resolved anomalies now read "just now" via formatRelativeFrom (hub/view-anomalies.ts:136)
  • leaveAnomaly carries both tiers' destinations, asserted in anomaly-rule.test.ts:52
  • Hub Anomalies view reads openIncidents for the open side (hub/view-anomalies.ts:207)
Observability coverage: 0 of 1 changes observable
Change Kind Observable Evidence
incident page → anomalies.getIncident / getIncidentTimeseries outbound call no FetchIncidentAnomaly/Timeseries (incident/command.ts) reuse the existing MapleApiV2Client calls in apps/web, which emit no client spans; the dashboard sends no telemetry of its own

5301042 · Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

Annotations

Check warning on line 138 in apps/web/src/page/alerts/editor/derive.ts

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

correctness: An anomaly rule routed by an escalation chain cannot be saved

`anomalyIssues` counts only `destinationIds` and `escalation.destinationIds`, while `isNotifyMissingOf` (`editor/view.ts:112`) also counts `escalationChainId` and `escalation.escalationChainId` as routing. A scoped anomaly rule whose tier starts a chain shows "Who gets notified" as `done` but `saveIssues` adds "Who gets notified", so the action bar prints "Fix before saving" and Save stays disabled.

Check warning on line 163 in packages/web-data/src/alerts/rule-modes.ts

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

correctness: Leaving Anomaly mode for SQL skips `carryThreshold`, saving a threshold 100× too high

`leaveAnomaly` restores the Built-in error-rate default threshold (a percent form value), and the SQL branch returns it verbatim, skipping the `carryThreshold` the Built-in → SQL branch at line 165 applies. `formThresholdToDomain` converts percent only for percent signals, so `raw_query` stores the raw number: switching an anomaly rule to Query mode saves `5` where an equivalent SQL rule saves `0.05`.