@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 +59 -0
- package/README.md +72 -7
- package/bundle.zip +0 -0
- package/package.json +10 -10
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),
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
52
|
-
"@calebcall/camera-ui-notify-darwin-amd64": "0.
|
|
53
|
-
"@calebcall/camera-ui-notify-linux-amd64": "0.
|
|
54
|
-
"@calebcall/camera-ui-notify-linux-arm64": "0.
|
|
55
|
-
"@calebcall/camera-ui-notify-windows-amd64": "0.
|
|
56
|
-
"@calebcall/camera-ui-notify-windows-arm64": "0.
|
|
57
|
-
"@calebcall/camera-ui-notify-linux-amd64-musl": "0.
|
|
58
|
-
"@calebcall/camera-ui-notify-linux-arm64-musl": "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",
|