@calebcall/camera-ui-notify 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.0] - 2026-08-12
11
+
12
+ ### Added
13
+
14
+ - **Video clips are surfaced on every backend.** camera.ui 2.1.6 added `videoUrl` to the
15
+ notification payload — a short MP4 of the recording behind the alert, published when a camera
16
+ (or an episode) has **Video in Push** enabled. Whether a clip is published at all is decided
17
+ there, not here; this is what every backend now does with one by default. None of them is the
18
+ first-party app, so none plays the clip inside the push itself; each offers it as a link the
19
+ phone opens in its own player, and in every case the clip is **added, never substituted** — the
20
+ snapshot keeps its attachment slot and the deep link keeps its click target. (Telegram and
21
+ Discord can go further; see the opt-in below.)
22
+ - **ntfy** — a `Play clip` view action button. ntfy allows one attachment per message and that
23
+ stays with the snapshot, which is the part every client renders inline. Published as a JSON
24
+ `Actions` header so a signed clip URL's commas and semicolons survive.
25
+ - **Telegram** — a second inline-keyboard button below the deep-link button.
26
+ - **Discord** — a `Play clip` link at the end of the embed description, with the target wrapped
27
+ in `<>` so a URL containing a closing paren still parses as one link.
28
+ - **Pushover** — the supplementary `url` (titled `Play clip`) when no deep link has claimed it,
29
+ otherwise a line appended to the message.
30
+ - **Gotify** — a `Play clip: <url>` line appended to the message text.
31
+ - **Grafana** — an anchor in the annotation tooltip (annotations mode); a `video_url`
32
+ annotation alongside `image_url` in alertmanager and IRM modes, for downstream templates.
33
+ - **Generic webhook** — a `videoUrl` field in the JSON payload.
34
+
35
+ - **"Upload video clips" setting for Telegram and Discord** (`telegram_clip` / `discord_clip`) —
36
+ off by default. Both services can carry the video itself and render a real player, so with this
37
+ on the plugin downloads the clip from camera.ui and re-uploads the bytes. Nothing outside your
38
+ network fetches from your server: the plugin is the only client that ever touches the clip URL,
39
+ so this works on an install that isn't reachable from the internet.
40
+ - **Telegram** posts `sendVideo` with `supports_streaming`, so the clip plays while it
41
+ downloads. Telegram carries one media item per message, so the clip replaces the snapshot.
42
+ Capped at the Bot API's own 50 MB upload limit. A silent follow-up costs nothing extra —
43
+ `editMessageCaption` leaves the video in place, so the clip is not re-downloaded for it.
44
+ - **Discord** attaches the clip as `clip.mp4` beside the embed, so the snapshot is kept. Capped
45
+ at 8 MB, leaving headroom under Discord's 10 MB per-request limit for the snapshot in the same
46
+ body. Discord's edit endpoint drops any attachment the request doesn't re-send, so a follow-up
47
+ costs a second download and upload.
48
+ - **Every failure degrades to the link rather than losing the notification**: a clip past the
49
+ cap is not downloaded at all (an over-limit `Content-Length` fails before the body is read), a
50
+ download that errors or stalls retries as an ordinary send, and so does an upload the service
51
+ rejects. Oversize clips are rejected, never truncated — a clipped MP4 is a broken upload, not
52
+ a smaller video. Each fallback is logged with its reason.
53
+
54
+ - **The camera.ui Base URL setting now absolutizes a relative clip URL too**, exactly as it
55
+ already did for deep links. camera.ui publishes `videoUrl` absolute, but a publisher that sends
56
+ a server-relative path would otherwise hand the backends a link no phone could open. Without
57
+ `base_url` set, a relative clip URL is dropped rather than delivered dead — except on the
58
+ generic webhook, whose receiver is a machine that may be able to resolve it, so there it is
59
+ forwarded verbatim.
60
+
61
+ ### Changed
62
+
63
+ - **camera.ui Go SDK bumped to v1.2.24** (from v1.2.22) for `Notification.VideoURL`.
64
+ - **`engines.camera.ui` raised to `>=2.1.6`** (from `>=2.0.24`), the release that publishes
65
+ `videoUrl`. Nothing in this version breaks on an older core — the field is simply absent and
66
+ every backend behaves as it did in 0.8.0 — but the plugin is now versioned against the core it
67
+ is written for.
68
+
10
69
  ## [0.8.0] - 2026-08-11
11
70
 
12
71
  ### Fixed
package/README.md CHANGED
@@ -45,7 +45,7 @@ Publishes to [ntfy.sh](https://ntfy.sh) or a self-hosted ntfy server.
45
45
  | `topic` | yes | — | The ntfy topic to publish to. |
46
46
  | `token` | no | — | Access token for a protected/self-hosted topic, sent as `Authorization: Bearer <token>`. |
47
47
 
48
- Delivery: `POST {server}/{topic}` with the notification body as the request body, plus `Title`, `Priority` (1–5, from severity), `Click` (deep link, if set), and `Attach`/`Icon` (image URL, if set) headers.
48
+ Delivery: `POST {server}/{topic}` with the notification body as the request body, plus `Title`, `Priority` (1–5, from severity), `Click` (deep link, if set), `Attach`/`Icon` (image URL, if set), and `Actions` (a "Play clip" view button, if a video clip is set) headers.
49
49
 
50
50
  ### Gotify
51
51
 
@@ -56,7 +56,7 @@ Publishes to a self-hosted [Gotify](https://gotify.net) server.
56
56
  | `server` | yes | Base URL of the Gotify server. Trailing `/` trimmed. |
57
57
  | `token` | yes | Gotify application token, used to authenticate published messages. |
58
58
 
59
- Delivery: `POST {server}/message?token={token}` with a JSON body `{title, message, priority}` (priority 0–10, from severity), plus a `client::notification.click` extra for the deep link and a `bigImageUrl` extra when an image URL is set.
59
+ Delivery: `POST {server}/message?token={token}` with a JSON body `{title, message, priority}` (priority 0–10, from severity), plus a `client::notification.click` extra for the deep link and a `bigImageUrl` extra when an image URL is set. A video clip is appended to the message text as a `Play clip: <url>` line.
60
60
 
61
61
  ### Generic webhook
62
62
 
@@ -69,7 +69,7 @@ Delivers to any HTTP endpoint you provide — the fallback for anything without
69
69
  | `headerName` | no | — | Optional custom header name (e.g. for a shared secret). Requires `headerValue` if set. |
70
70
  | `headerValue` | no | — | Value of the custom header. Requires `headerName` if set. |
71
71
 
72
- Delivery: `{method} {url}` with `Content-Type: application/json` and (if configured) the custom header, carrying a JSON body of `{title, subtitle, body, severity, tag, silent, imageUrl, deepLink, data, createdAt, thumbnailBase64}`.
72
+ Delivery: `{method} {url}` with `Content-Type: application/json` and (if configured) the custom header, carrying a JSON body of `{title, subtitle, body, severity, tag, silent, imageUrl, videoUrl, deepLink, data, createdAt, thumbnailBase64}`.
73
73
 
74
74
  ### Pushover
75
75
 
@@ -80,7 +80,7 @@ Hosted push to the [Pushover](https://pushover.net) app.
80
80
  | `token` | yes | Pushover application API token/key. |
81
81
  | `user` | yes | Your Pushover user or group key. |
82
82
 
83
- Delivery: `POST https://api.pushover.net/1/messages.json` with title/message and a priority (Info→0 normal, everything higher→1 high; never emergency). The snapshot **image** is sent as an `attachment`, and an absolute deep link becomes a supplementary `url` titled "Open camera" for a detection or "Open in camera.ui" for anything else.
83
+ Delivery: `POST https://api.pushover.net/1/messages.json` with title/message and a priority (Info→0 normal, everything higher→1 high; never emergency). The snapshot **image** is sent as an `attachment`, and an absolute deep link becomes a supplementary `url` titled "Open camera" for a detection or "Open in camera.ui" for anything else. A video clip takes that `url` slot when no deep link claims it, and otherwise appends a `Play clip: <url>` line to the message.
84
84
 
85
85
  ### Telegram
86
86
 
@@ -90,8 +90,9 @@ Delivers to a chat via a [Telegram bot](https://core.telegram.org/bots).
90
90
  | ------- | -------- | -------------------------------------------------------- |
91
91
  | `token` | yes | Bot token from @BotFather. |
92
92
  | `chat` | yes | Chat ID to deliver to. |
93
+ | `clip` | no | **Upload video clips** — off by default. See [Video clips](#video-clips-video-in-push). |
93
94
 
94
- Delivery: `sendPhoto` (with the snapshot **image** + caption) when a thumbnail is present, otherwise `sendMessage`. An absolute deep link is added as an inline button, labelled "Open camera" when it opens a camera page and "Open in camera.ui" otherwise.
95
+ Delivery: `sendPhoto` (with the snapshot **image** + caption) when a thumbnail is present, otherwise `sendMessage` — or `sendVideo` when clip upload is on and the notification carries a clip. An absolute deep link is added as an inline button, labelled "Open camera" when it opens a camera page and "Open in camera.ui" otherwise; a video clip adds a second "Play clip" button below it, unless it is being uploaded.
95
96
 
96
97
  ### Discord
97
98
 
@@ -100,8 +101,9 @@ Delivers to a channel via a Discord [webhook](https://support.discord.com/hc/en-
100
101
  | Field | Required | Notes |
101
102
  | --------- | -------- | --------------------------------------- |
102
103
  | `webhook` | yes | Channel webhook URL. |
104
+ | `clip` | no | **Upload video clips** — off by default. See [Video clips](#video-clips-video-in-push). |
103
105
 
104
- Delivery: a rich embed (title, body, severity color — blue/yellow/red) with the snapshot **image** attached; an absolute deep link makes the title a link.
106
+ Delivery: a rich embed (title, body, severity color — blue/yellow/red) with the snapshot **image** attached; an absolute deep link makes the title a link, and a video clip is appended to the description as a `Play clip` link — or uploaded as a second attachment when clip upload is on.
105
107
 
106
108
  ### Grafana
107
109
 
@@ -204,6 +206,10 @@ timer and per-event state, and a restart would strand the group open anyway. Clo
204
206
  > annotations have no image field at all, and alerts carry the URL as an `image_url` annotation
205
207
  > that Grafana itself won't render but downstream notification templates can use.
206
208
 
209
+ > **Video clips:** annotations mode adds a `Play clip` anchor to the annotation tooltip; the
210
+ > alertmanager and IRM modes carry the URL as a `video_url` annotation, alongside `image_url` and
211
+ > on the same terms — Grafana won't render it, downstream templates can use it.
212
+
207
213
  > **Secrets in logs:** transport failures never log the bot token / webhook URL / other
208
214
  > URL-embedded secret — request URLs are redacted from delivery errors.
209
215
 
@@ -233,6 +239,64 @@ Only the `silent` follow-up replaces. Detection tags repeat across events (`moti
233
239
 
234
240
  If you would rather never see the follow-up on a backend that can't replace, set **Follow-up updates** to `Skip the update entirely` in the plugin settings. Backends that replace in place still receive it under that setting, since editing adds nothing to the notification list — with the one caveat that a lost message id (restart, deleted message) turns that edit into a new quiet message.
235
241
 
242
+ ## Video clips ("Video in Push")
243
+
244
+ camera.ui 2.1.6 added `videoUrl` to the notification payload: a short MP4 of the recording that
245
+ triggered the alert, published when the camera — or, for a multi-camera episode, the episode —
246
+ has **Video in Push** switched on under its notification settings. Whether a clip is published at
247
+ all is decided there, per camera, not here — this plugin only forwards what it is handed.
248
+
249
+ ### By default: the clip is a link
250
+
251
+ Nothing this plugin talks to is the first-party mobile app, so no backend plays the clip *inside*
252
+ the push the way an iOS attachment does. What each one can do is offer the clip as a link, opened
253
+ in the phone's own browser or player — already authenticated to your server. The rule is the same
254
+ everywhere: **the clip is added, never substituted.** The snapshot keeps its attachment slot and
255
+ the deep link keeps its click target, because the deep link opens the event in camera.ui, from
256
+ which the recording is one tap away, whereas the clip on its own is a dead end.
257
+
258
+ | Backend | How the clip is surfaced |
259
+ | --------------- | ----------------------------------------------------------------------------------- |
260
+ | ntfy | A `Play clip` **view action button** (the single `Attach` slot stays with the snapshot). |
261
+ | Telegram | A second **inline button** below the deep-link button. |
262
+ | Discord | A `Play clip` link at the end of the embed description. |
263
+ | Pushover | The supplementary `url` when no deep link claims it; otherwise a line in the message. |
264
+ | Gotify | A `Play clip: <url>` line appended to the message text. |
265
+ | Grafana | An anchor in the annotation tooltip; a `video_url` annotation in alertmanager/IRM modes. |
266
+ | Generic webhook | A `videoUrl` field in the JSON payload, forwarded verbatim. |
267
+
268
+ **A linked clip has to be reachable from the phone.** camera.ui publishes the URL absolute; a
269
+ server-relative one is made absolute with the **camera.ui Base URL** setting, the same way deep
270
+ links are. Without that setting a relative clip URL is dropped rather than delivered as a link
271
+ that cannot open — except on the generic webhook, whose receiver is a machine that may well be
272
+ able to resolve it, so there it is forwarded as-is.
273
+
274
+ ### Opt in: upload the clip (Telegram, Discord)
275
+
276
+ Telegram and Discord can carry the video itself, and both render a real player in the chat. Turn
277
+ on **Upload video clips** in the plugin settings for either one and the plugin downloads the clip
278
+ from camera.ui and re-uploads the bytes to the service. Nothing outside your network ever fetches
279
+ from your server — the plugin is the only client that touches the clip URL — so this works for an
280
+ install that isn't reachable from the internet at all.
281
+
282
+ | | Telegram | Discord |
283
+ | --- | --- | --- |
284
+ | Method | `sendVideo`, `supports_streaming` on so it plays while downloading | a `clip.mp4` file attachment beside the embed |
285
+ | Snapshot | **replaced** — Telegram carries one media item per message | **kept**, still rendered inside the embed |
286
+ | Size cap | 50 MB (the Bot API's own upload limit) | 8 MB (Discord allows 10 MB per request on an unboosted server; the headroom is for the snapshot) |
287
+ | Follow-up cost | none — `editMessageCaption` leaves the video in place, so the AI description doesn't re-download it | one more download + upload — Discord's edit drops any attachment the request doesn't re-send |
288
+
289
+ It is off by default because it is the expensive path: the default merely passes a URL along,
290
+ while this moves the whole file twice for every detection. The Discord row above is the one to
291
+ weigh — a busy camera with clip upload on moves each clip **four** times once the AI description
292
+ lands.
293
+
294
+ **Every failure falls back to the link, never to a lost notification.** A clip past the size cap
295
+ is not downloaded at all (a `Content-Length` over the limit fails before the body is read); a
296
+ download that errors or stalls, and an upload the service rejects, both retry immediately as an
297
+ ordinary send with the `Play clip` link. Oversize clips are rejected rather than truncated — a
298
+ clipped MP4 is a broken upload, not a smaller video. Each fallback is logged with its reason.
299
+
236
300
  ## Configuring your target (v1: one active target)
237
301
 
238
302
  There is no "add device" flow. Instead, configure the plugin itself:
@@ -241,7 +305,8 @@ There is no "add device" flow. Instead, configure the plugin itself:
241
305
  2. In its settings, pick a **Service** (`ntfy`, `Gotify`, `Generic webhook`, `Pushover`, `Telegram`, `Discord`, or `Grafana`) from the dropdown built from the registered backends.
242
306
  3. Fill in that service's fields — only the selected service's fields are shown; the rest are condition-gated out.
243
307
  4. Optionally set **Follow-up updates** (see [Follow-up updates](#follow-up-updates-ai-descriptions)) — defaults to delivering the AI description quietly.
244
- 5. Save. The config is validated (`ParseTarget`) the next time a notification is dispatched; `getDevices` then synthesizes one delivery target from it, and notifications from any publisher are delivered there.
308
+ 5. On Telegram or Discord, optionally turn on **Upload video clips** (see [Video clips](#video-clips-video-in-push)) off by default.
309
+ 6. Save. The config is validated (`ParseTarget`) the next time a notification is dispatched; `getDevices` then synthesizes one delivery target from it, and notifications from any publisher are delivered there.
245
310
 
246
311
  This is a **single, instance-wide target** in v1 — there's no way to register several devices at once. Changing the config replaces the previous target rather than adding to it. Delivery for that one target is a single request per notification (no fan-out to worry about, since there's only one device).
247
312
 
package/bundle.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "Notify",
3
3
  "name": "@calebcall/camera-ui-notify",
4
- "version": "0.8.0",
4
+ "version": "0.9.0",
5
5
  "description": "A fully-local, multi-backend camera.ui notifier plugin: delivers notifications to ntfy, Gotify, Pushover, Telegram, Discord, Grafana, or a generic webhook, entirely on your own hardware with no cloud dependency.",
6
6
  "author": "calebcall (https://github.com/calebcall/camera-ui-notify)",
7
7
  "main": "./main.go",
@@ -25,7 +25,7 @@
25
25
  "url": "https://github.com/calebcall/camera-ui-notify/issues"
26
26
  },
27
27
  "engines": {
28
- "camera.ui": ">=2.0.24",
28
+ "camera.ui": ">=2.1.6",
29
29
  "node": ">=22.0.0"
30
30
  },
31
31
  "homepage": "https://github.com/calebcall/camera-ui-notify/tree/main#readme",
@@ -48,14 +48,14 @@
48
48
  "protocolLevel": 1
49
49
  },
50
50
  "optionalDependencies": {
51
- "@calebcall/camera-ui-notify-darwin-arm64": "0.8.0",
52
- "@calebcall/camera-ui-notify-darwin-amd64": "0.8.0",
53
- "@calebcall/camera-ui-notify-linux-amd64": "0.8.0",
54
- "@calebcall/camera-ui-notify-linux-arm64": "0.8.0",
55
- "@calebcall/camera-ui-notify-windows-amd64": "0.8.0",
56
- "@calebcall/camera-ui-notify-windows-arm64": "0.8.0",
57
- "@calebcall/camera-ui-notify-linux-amd64-musl": "0.8.0",
58
- "@calebcall/camera-ui-notify-linux-arm64-musl": "0.8.0"
51
+ "@calebcall/camera-ui-notify-darwin-arm64": "0.9.0",
52
+ "@calebcall/camera-ui-notify-darwin-amd64": "0.9.0",
53
+ "@calebcall/camera-ui-notify-linux-amd64": "0.9.0",
54
+ "@calebcall/camera-ui-notify-linux-arm64": "0.9.0",
55
+ "@calebcall/camera-ui-notify-windows-amd64": "0.9.0",
56
+ "@calebcall/camera-ui-notify-windows-arm64": "0.9.0",
57
+ "@calebcall/camera-ui-notify-linux-amd64-musl": "0.9.0",
58
+ "@calebcall/camera-ui-notify-linux-arm64-musl": "0.9.0"
59
59
  },
60
60
  "files": [
61
61
  "bundle.zip",