A monitor re-captures a URL on a schedule and diffs the normalized content
(line-trimmed markdown, SHA-256). On change it emits a webhook event and keeps a
snapshot for later diffing.

## Endpoints

```
POST   /v1/monitor                 create
GET    /v1/monitor                 list (keyset)
GET    /v1/monitor/{id}            detail
PATCH  /v1/monitor/{id}            { "status": "paused" | "active" } | interval | ignore_patterns | wait_for
DELETE /v1/monitor/{id}            delete (cascades checks + snapshots)
GET    /v1/monitor/{id}/checks     check history (keyset); changed/removed rows carry
                                 an inline diff_excerpt (bounded change summary)
GET    /v1/monitor/{id}/checks/{checkId}/diff   adjacent-snapshot line diff (free)
GET    /v1/monitor/{id}/snapshots/{snapshotId}  snapshot source: normalized markdown
                                 (text/markdown, not the JSON envelope; free)
POST   /v1/monitor/{id}/run        trigger a manual check now → 202
```

Create body: `{url, name?, interval_minutes?, ignore_patterns?, webhook_id?, wait_for?}`.

## Semantics

- **Interval floor is per-plan** — e.g. Free gets a coarser minimum than paid
  plans; too-small values are rejected with `INVALID_REQUEST`.
- **`wait_for` (ms, 0–60000, optional)** — extra wait after page load before
  capture, same field as `/v1/markdown`. Pages that hydrate their content with
  JS (app-like directories, dashboards, filtered lists) need it: without it the
  snapshot may only contain page meta/boilerplate and changes to the real
  content are never seen. If the first check's snapshot looks like front-matter
  only, PATCH `{"wait_for": 5000}` — the next check re-captures as `changed`
  (a legit baseline re-cut, not an error). Content below the rendered viewport
  (infinite scroll) is still out of scope.
- Each check costs **1 credit**; errored checks cost 0.
- `ignore_patterns`: regex lines stripped **before** hashing — use for timestamps,
  view counters, ad text that changes every render.
- Check statuses: `new` (first baseline) · `same` · `changed` · `removed` · `error`.
- **Circuit breaker**: after consecutive errors the monitor auto-pauses and
  notifies you — fix the target and resume with `PATCH`.
- Snapshots are stored on `new`/`changed` only (bounded retention per monitor).
- **Diff endpoint**: `GET .../checks/{checkId}/diff` returns a bounded hunk-style
  line diff (`context` param, `elision` segments, `stats`, `truncated`).
  `available:false` + `reason` is a normal state, not an error —
  `not_diffable` (same/error), `snapshot_expired` (snapshot pruned by
  retention: snapshots outlive ~10 changes, checks 200), `too_large`.
- **Snapshot source endpoint**: `GET .../snapshots/{snapshotId}` returns the
  stored normalized markdown (line-trimmed, `ignore_patterns` lines stripped —
  not the raw page) as `text/markdown`, with `ETag`=content_hash and
  `X-Snapshot-Char-Count`/`X-Snapshot-Created-At` headers. Check rows expose
  `snapshot_id` (content written by that check) and `prev_snapshot_id` (the
  baseline — for `removed`/`same` rows this is the content that was seen).
  Only the latest ~10 snapshots per monitor are kept: a `404` usually means
  the snapshot was pruned by retention, so fetch soon after the event.

## Notifications

Monitors notify via your team's notification channels — subscribe to
`monitor.*` events in dashboard → Notifications, or pin a specific channel
with `webhook_id`. Use `POST /v1/monitor/{id}/run` to verify the wiring
without waiting for the schedule.