# Phase I — Activation Watcher Real-Time Stream

**Branch**: `feat/fraud-activation-watcher`
**Parent**: `feat/fraud-rules-jube-parity` (rebased after Phase J merges, for case_id link).
**Jube reference**: `ActivationWatcher` SignalR stream + Image-4 "Activation Watcher" page.

---

## 1. Goal

Every Activation rule that fires with `send_to_activation_watcher = true`
produces a row in `risk_activation_watcher`. The new LiveView streams those
rows to ops dashboards in real time (sub-second), with colour coding driven
by `response_elevation_*_color` and a click-through to the linked case
(Phase J).

---

## 2. Architectural pieces

| Concern | Module |
|---|---|
| **Producer** (writes row + broadcasts) | `MwRisk.ActivationWatcherProducer` (new) |
| **Schema**                              | [`risk_activation_watcher.ex`](../../../apps/infra_repo/lib/infra_repo/schemas/risk_activation_watcher.ex) ✅ |
| **PubSub topic**                        | `"risk:watcher:<tenant_id>"` carrying `{:activation, %{…}}` |
| **LiveView**                            | `ActivationWatcherLive` at `/admin/fraud/activation-watcher` |
| **Backpressure**                        | `MwRisk.WatcherThrottle` — drop-oldest at 500-row LiveView buffer |

---

## 3. Work breakdown

### 3.1 Producer

Hook into the existing `SideEffectDispatcher` (Phase E):

```elixir
defp dispatch_one(%{kind: :case, details: details} = effect, tenant_id) do
  # existing case-open dispatch
  MwRisk.ActivationWatcherProducer.publish(tenant_id, details)
  ...
end
```

`ActivationWatcherProducer.publish/2`:
1. Builds an `%RiskActivationWatcher{}` row from `details` + `payload_snapshot`.
2. `Repo.insert/1` (async via `Task.Supervisor.start_child` so scoring stays fast).
3. `Phoenix.PubSub.broadcast(MwCore.PubSub, "risk:watcher:#{tenant_id}", {:activation, row})`.

Add a config switch `:mw_risk, :watcher_async, true` so tests can flip to sync.

### 3.2 LiveView

`apps/gateway_web/lib/gateway_web_web/live/admin/fraud/activation_watcher_live.ex`
at `/admin/fraud/activation-watcher`.

- `mount/3` subscribes to `"risk:watcher:#{tenant_id}"`.
- Uses `stream/3` (Phoenix LiveView streams) with `limit: 500`.
- Filters (top bar): entity_model · rule (multi-select) · min response_elevation slider · entity_key text.
- Row template: timestamp · rule name · entity_value · score chip (coloured by `response_elevation`) · case link (if `case_id` non-nil) · "View payload" → modal showing JSON snapshot.
- Pause / resume button to halt the stream when triaging.

### 3.3 Throttling

If the producer broadcasts > 50 events / sec, `WatcherThrottle` aggregates them
into 100 ms batches before sending to PubSub so each LiveView gets a single
`handle_info/2` per tick. Implemented as a small GenServer per tenant under
a `Registry`-keyed DynamicSupervisor.

### 3.4 Audit + retention

- Add a daily Oban job to delete watcher rows older than 30 days (config:
  `:mw_risk, :watcher_retention_days`).
- Index `(tenant_id, scored_at desc)` if not already present.

### 3.5 Tests

- Producer test: insert + broadcast on rule fire (use `Phoenix.PubSub` subscriber).
- LiveView test: subscribe → trigger producer → assert row appears, filters work, pause stops new rows.
- Throttle test: 1000 events in 1s → ≤ 20 PubSub messages.

### 3.6 Seeds

Mark 1–2 seeded Activation rules with `send_to_activation_watcher: true` so a
demo run produces visible rows.

---

## 4. Acceptance criteria

- [ ] Firing an Activation rule with the watcher toggle creates a `risk_activation_watcher` row within 50 ms (async path).
- [ ] An open LiveView session sees the new row appear within 200 ms of insert.
- [ ] Throttling caps PubSub delivery to ≤ 10 Hz under load.
- [ ] Retention job deletes rows older than 30 days.
- [ ] Watcher row's `case_id` (when populated post-J) links to `/admin/fraud/cases/:id`.

---

## 5. Out of scope

- Map widget (Jube renders lat/long pins) — schema has lat/long; map view ships separately.
- Per-user saved filter presets.
- CSV export of the live feed (use `risk_activation_watcher` table query).
- WebSocket push to external dashboards (subscribe to PubSub directly from another app).
