# Metric Alerts

## Purpose

Metric alerts watch a patient's incoming health measures and send a notification
when a value crosses a configured threshold (for example *heart rate average
greater than 100 bpm*, or *temperature greater than 38 °C*). They let patients
and caregivers react to anomalies in the data synced from connected devices.

Alerts are distinct from [Reminders](reminders.md): a reminder is a scheduled
message, while an alert is condition-driven. When an alert fires it *creates* a
reminder that keeps re-notifying until the alert is acknowledged.

## Who can create them

**Admin**, **company admin**, and **medical operator** can create alerts for a
patient (via the alert policy). Each alert belongs to one patient.

## Anatomy of an alert

An alert is a metric plus a condition:

- **metric** — what to watch (see [Supported metrics](#supported-metrics)).
- **type** — `greater` or `lower` (fire when the value is above / below the threshold).
- **threshold** — the numeric value to compare against.
- **period** — a `value` + `unit` pair, with unit `minutes`, `hours`, or `days`
  (e.g. `2 days`). Its meaning depends on the metric family — see
  [Period semantics](#period-semantics).
- **channel** — `mail` or `whatsapp`.
- **recipients** — the patient and/or their related contacts.
- **reminder settings** — how the confirmation reminder repeats after the alert
  fires (`max sends`, interval `value` + `unit`).

## When alerts are evaluated

Alerts are **not** checked on a fixed schedule. They are evaluated **whenever a
new measure arrives** for the patient, so the freshest data is always used. The
evaluation is triggered from:

| Source | Metrics checked |
| --- | --- |
| GIMAHub webhook (`ProcessGimaHubWebhookEvent`) | the metrics mapped from the incoming measure type (blood pressure, holter, temperature, glucose, spirometry, ECG, body weight, lab, SpO2/heart rate, steps) |
| Withings `SyncAutomaticMeasures` | heart-rate average, SpO2 average, steps total |
| Withings `SyncManualMeasures` | heart-rate average, SpO2 average |
| Withings `SyncSleepMeasures` | sleep average |
| Withings `SyncBodyCompositionMeasures` | body weight |

`Alert::checkForMetrics($patient, [$metric, …])` loads the patient's alerts for
those metrics and evaluates each one.

## Supported metrics

Two evaluation families exist. The family decides how the **period** is
interpreted (see [Period semantics](#period-semantics)).

### Aggregate metrics

The value compared to the threshold is an **aggregate over the whole period**.

| Metric key | Source | Unit | Aggregate |
| --- | --- | --- | --- |
| `heart-rate-average` | Heart rate | bpm | average |
| `oxygen-saturation-average` | SpO2 | % | average |
| `sleep-average` | Sleep (nightly duration) | hours | average |
| `steps-total` | Activity | steps | total (sum) |

### Latest-measure metrics

The value compared to the threshold is the **single most recent measurement**,
provided it falls inside the period (see below). Introduced to extend alerts to
every monitored instrument.

| Metric key | Source | Unit |
| --- | --- | --- |
| `blood-pressure-systolic` | Blood pressure | mmHg |
| `blood-pressure-diastolic` | Blood pressure | mmHg |
| `holter-blood-pressure-systolic` | Ambulatory BP (holter) | mmHg |
| `holter-blood-pressure-diastolic` | Ambulatory BP (holter) | mmHg |
| `temperature` | Temperature | °C |
| `glucose` | Glucose | mg/dL |
| `spirometry-fev1` | Spirometry | L |
| `spirometry-pef` | Spirometry | L/s |
| `ecg-heart-rate` | ECG | bpm |
| `body-weight` | Body composition | kg |
| `lab-hemoglobin` | Lab | g/dL |
| `lab-ketone` | Lab | mmol/L |
| `lab-cholesterol` | Lab | mg/dL |
| `lab-lactate` | Lab | mmol/L |
| `lab-uric-acid` | Lab | mg/dL |
| `lab-triglycerides` | Lab | mg/dL |

Lab metrics share the `lab_measures` table and are told apart by their `type`.

## Period semantics

The period (e.g. *"in the last 2 days"*) means **two different things**
depending on the metric family. This is the part most easily misread.

### Aggregate metrics → aggregation window

The period is the window over which the aggregate is computed. All measures with
a timestamp within `now − period` are averaged (or summed, for steps), and that
result is compared to the threshold.

> *"Heart-rate average greater than 100 bpm in the last 2 days"* → average every
> heart-rate reading of the last 48 hours; alert if that average exceeds 100.

### Latest-measure metrics → freshness window

The period does **not** aggregate anything. The system takes only the most
recent measurement and uses the period as a **validity/freshness filter**: the
measure counts only if it was taken within `now − period`. If the latest
measurement is older than the period, it is treated as *no data* and no alert is
sent (a stale reading never triggers an alarm).

> *"Temperature greater than 38 °C in the last 5 days"* → look at the latest
> temperature; if it was taken within 5 days and is above 38, the alert fires
> **immediately** (it does not wait 5 days). If the most recent reading is 6 days
> old, it is ignored.

The window is rolling and evaluated at the instant a measure arrives, using a
strict comparison (`measured_at > now − period`).

## Notification lifecycle (once per episode)

An alert notifies **once per episode**, not on every incoming measure, tracked by
the `triggered_at` column:

1. **Condition becomes true, no open episode** (`triggered_at` is null) — the
   alert fires: it sends the notification, sets `triggered_at`, and starts a
   confirmation reminder that keeps re-notifying on its configured interval
   until acknowledged or `max sends` is reached.
2. **Condition stays true** — the alert does **not** notify again; follow-ups are
   handled by the reminder chain. It re-alerts only if the alert went
   **unacknowledged for a whole period** (stale), so an ignored alert cannot
   silence new episodes indefinitely.
3. **Value returns in range** — the episode closes: `triggered_at` is cleared and
   the pending unconfirmed reminders are disabled. The next time the threshold is
   crossed, the alert fires as a fresh episode.

## Related

- [Reminders](reminders.md) — the confirmation reminders spawned by a firing alert.
- [Notifications](notifications.md) — delivery channels (email / WhatsApp).
- [Business logic](../business-logic.md#alerts) — where alerts sit in the domain.
