# Incident Webhooks

`com.glidevizion.incident-webhooks` turns durable detections into short,
high-quality incident clips without giving the sidecar recording paths, SQLite
access, RTSP credentials, or unrestricted camera access.

The plugin requests four reviewed capabilities:

- `detections:read` to consume the ordered detection feed;
- `recordings:read` to request completed MP4 clips;
- `network:outbound` to call the operator's destination; and
- `ui:settings.panel` for its generic settings form.

Both read capabilities are independently restricted to the cameras granted by
the operator. The configured camera must also appear under **Camera access**.

## Delivery contract

For each matching detection the plugin derives a clip window around
`playback_ms` (falling back to `time_ms`), requests the configured recorded
stream, and sends one `multipart/form-data` request:

- `metadata`: `application/json` with `plugin_id`, `camera_id`, `delivery_id`,
  the detection, and the exact clip range;
- `clip`: `video/mp4`, named `<camera>-detection-<id>.mp4`.

Every request includes:

```text
X-NVR-Plugin-ID: com.glidevizion.incident-webhooks
X-NVR-Delivery-ID: <camera>:incident:<detection-id>
Authorization: Bearer <destination token>   # only when configured
```

The MP4 is streamed from nvrd to the destination and is never buffered in full
by the plugin. A delivery is capped at 128 MiB. The daemon builds the clip with
codec copy, so this plugin does not consume GPU or re-encode footage. nvrd waits
until the whole requested pre/post window is finalized before returning it;
while post-roll is still open, the detection remains pending. The authenticated
response is `private, no-store`, although nvrd may reuse its private disk cache.

Any `2xx` response acknowledges the incident. Delivery is at least once: the
insertion-order cursor is persisted only after acknowledgement, so a timeout,
rejected upload, crash, or clip-fetch failure retries the same stable delivery
ID. The first-run `from_ms` applies the configured lookback; after the first row,
`id > after_id` includes detections inserted later even when their event time is
older. Destinations should deduplicate on `X-NVR-Delivery-ID`.

Rows rejected by the source, label, or confidence filters advance the cursor
without downloading video. This prevents irrelevant detections from blocking
newer incidents.

## Defaults

- recording quality: `main`;
- pre-roll: 5 seconds;
- post-roll: 10 seconds;
- minimum confidence: 0.5;
- first-run lookback: 60 seconds;
- healthy polling interval: 5 seconds.

Pre-roll and post-roll are independently limited to 60 seconds. If a selected
stream is not recorded for that camera, the plugin reports unhealthy and keeps
the detection pending instead of silently discarding the incident. Failed polls
back off to 10, 20, 40, then at most 60 seconds; one successful poll resets the
interval to five seconds.

Production destinations must use HTTPS. Plain HTTP is accepted only for
localhost or another loopback address during development. Redirects are not
followed, so the destination bearer token and video cannot move to a redirect
target.

## Local development

The supervisor injects `NVR_BASE_URL`, `NVR_API_TOKEN`, and
`NVR_PLUGIN_STATE_DIR`. Manifest settings are injected as their uppercase field
names. To run the binary manually, set at least:

```bash
NVR_BASE_URL=http://127.0.0.1:8085 \
NVR_API_TOKEN=nvrp_... \
NVR_PLUGIN_STATE_DIR=/tmp/incident-webhooks-state \
DESTINATION_URL=https://example.com/nvr/incidents \
CAMERA_ID=cam01 \
go run ./plugins/incident-webhooks/cmd/incident-webhooks
```

Build a signed package with `scripts/package-incident-webhooks 0.1.0` after
setting `NVR_MARKETPLACE_PRIVATE_KEY`. The archive contains one binary for the
selected `GOOS`/`GOARCH` (the current host by default); do not reuse one
`OUT`/version for multiple targets because it overwrites `package.zip`.
Marketplace publication still needs platform-aware artifacts and catalog
selection, a production signature, and a live destination acknowledgement.
