@calebcall/camera-ui-notify 0.7.1 → 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 +107 -0
- package/README.md +115 -9
- package/bundle.zip +0 -0
- package/package.json +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,113 @@ All notable changes to **Notify** are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
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
|
+
|
|
69
|
+
## [0.8.0] - 2026-08-11
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- **No more duplicate notifications for AI descriptions.** camera.ui announces a detection, then
|
|
74
|
+
republishes the same notification once the AI description is ready — same `tag`, plus
|
|
75
|
+
`silent: true` to mark it as an update rather than a new alert. Both fields were previously
|
|
76
|
+
ignored, so every detection arrived twice, both times with sound. Now:
|
|
77
|
+
- **Telegram** and **Discord** replace the original message in place (`editMessageText` /
|
|
78
|
+
`editMessageCaption`, `PATCH .../messages/{id}`), so one notification's text improves rather
|
|
79
|
+
than a second one appearing. Message ids are held in memory per tag for 15 minutes; after a
|
|
80
|
+
restart, or if the original was deleted, the update falls back to a new quiet message. Only
|
|
81
|
+
the `silent` follow-up replaces — detection tags repeat across events, so a new alert reusing
|
|
82
|
+
a tag always posts a new message rather than rewriting chat history.
|
|
83
|
+
- **ntfy** (priority `1`), **Gotify** (priority `3`) and **Pushover** (priority `-1`) deliver the
|
|
84
|
+
update without sound or vibration.
|
|
85
|
+
- **Grafana** resolves the update onto the event it already opened: annotations mode patches the
|
|
86
|
+
existing annotation's text, Alertmanager and IRM modes re-send under the same fingerprint so
|
|
87
|
+
the alert updates instead of a second one firing.
|
|
88
|
+
- The **generic webhook** payload forwards the new `silent` field alongside the existing `tag`.
|
|
89
|
+
- `critical` notifications ignore `silent` and always alert, per the SDK contract.
|
|
90
|
+
|
|
91
|
+
- **README no longer documents `grafana_irm_ttl`.** The field was removed from the code in 0.7.1
|
|
92
|
+
when IRM turned out to ignore `endsAt`, but its row survived in the config table — directly
|
|
93
|
+
contradicting the paragraph below it stating that IRM groups do not auto-resolve.
|
|
94
|
+
|
|
95
|
+
### Added
|
|
96
|
+
|
|
97
|
+
- **"Follow-up updates" setting** (`silent_updates`) — `Deliver quietly (no sound)` (default) or
|
|
98
|
+
`Skip the update entirely`, for anyone who wants exactly one notification per event on the
|
|
99
|
+
backends that can't replace a delivered message. Backends that replace in place still receive
|
|
100
|
+
the update under `skip`, since it adds no entry to the notification list.
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
|
|
104
|
+
- **camera.ui Go SDK bumped to v1.2.22** (from v1.2.6) for `Notification.Silent`.
|
|
105
|
+
- **Linting consolidated on golangci-lint**, configured by a new `.golangci.yml`. `npm run lint`
|
|
106
|
+
previously chained `staticcheck && golangci-lint run`, so a staticcheck finding short-circuited
|
|
107
|
+
the `&&` and golangci-lint never ran at all. golangci-lint already bundles staticcheck's
|
|
108
|
+
analyzers, so the standalone binary is gone from the script; the config excludes three
|
|
109
|
+
deliberate idioms (`defer resp.Body.Close()`, and nil-`Context` / De Morgan findings in tests)
|
|
110
|
+
and `npm run lint` now reports zero issues.
|
|
111
|
+
- **`package.json` description now lists every backend.** It had said "ntfy, Gotify, or a generic
|
|
112
|
+
webhook" since 0.4.0, omitting Pushover, Telegram, Discord and Grafana. This is the text npm
|
|
113
|
+
shows on the package listing. ([#29](https://github.com/calebcall/camera-ui-notify/issues/29))
|
|
114
|
+
|
|
8
115
|
## [0.7.1] - 2026-08-02
|
|
9
116
|
|
|
10
117
|
### 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, 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
|
|
|
@@ -121,7 +123,6 @@ different services, so each has its own connection fields.
|
|
|
121
123
|
| `grafana_alertname` | no | alertmanager | `alertname` label. Defaults to `CameraUINotification`. |
|
|
122
124
|
| `grafana_ttl` | no | alertmanager | Seconds before Alertmanager auto-resolves the alert. Default `900`, minimum `30`. |
|
|
123
125
|
| `grafana_irm_url` | yes | irm | Inbound webhook URL of an IRM / OnCall integration. The token is in the URL, so it is masked and kept out of every error message. |
|
|
124
|
-
| `grafana_irm_ttl` | no | irm | Seconds before the alert is eligible to auto-resolve. Default `300`, minimum `30`. Whether IRM acts on it depends on the integration's templates. |
|
|
125
126
|
|
|
126
127
|
**Annotations** — `POST {server}/api/annotations` with a point-in-time, organization-wide
|
|
127
128
|
annotation tagged `camera.ui`, `camera:<name>`, `severity:<level>`, plus your extra tags. Surface it
|
|
@@ -205,9 +206,97 @@ timer and per-event state, and a restart would strand the group open anyway. Clo
|
|
|
205
206
|
> annotations have no image field at all, and alerts carry the URL as an `image_url` annotation
|
|
206
207
|
> that Grafana itself won't render but downstream notification templates can use.
|
|
207
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
|
+
|
|
208
213
|
> **Secrets in logs:** transport failures never log the bot token / webhook URL / other
|
|
209
214
|
> URL-embedded secret — request URLs are redacted from delivery errors.
|
|
210
215
|
|
|
216
|
+
## Follow-up updates (AI descriptions)
|
|
217
|
+
|
|
218
|
+
camera.ui announces a detection immediately, then republishes the same notification a few seconds later once the AI description is ready. Both publishes carry the same `tag` (the collapse key), and the second carries `silent: true`, meaning *this only updates the first one — don't alert again*.
|
|
219
|
+
|
|
220
|
+
Handled per backend, according to what each platform can actually do:
|
|
221
|
+
|
|
222
|
+
| Backend | Follow-up behaviour |
|
|
223
|
+
| --------------- | ------------------------------------------------------------------------------------ |
|
|
224
|
+
| Telegram | **Replaces** the original message (`editMessageText` / `editMessageCaption`). |
|
|
225
|
+
| Discord | **Replaces** the original message (`PATCH .../messages/{id}`). |
|
|
226
|
+
| Grafana | **Revises** the record it already filed — see below. |
|
|
227
|
+
| ntfy | Delivered at priority `1` — visible, no sound or vibration. |
|
|
228
|
+
| Gotify | Delivered at priority `3` — joins the in-app list, raises no system notification. |
|
|
229
|
+
| Pushover | Delivered at priority `-1` (quiet) — no sound or vibration. |
|
|
230
|
+
| Generic webhook | `silent: true` is forwarded in the JSON payload; your endpoint decides. |
|
|
231
|
+
|
|
232
|
+
Grafana revises per mode: **annotations** patches the annotation it created (`PATCH /api/annotations/:id`), so the dashboard keeps one marker at the detection's own timestamp whose text improves. **Alertmanager** and **IRM** re-file under the `event_id` / `alert_uid` the first alert used — that identity is what each surface deduplicates on, so the existing alert or group picks up the description instead of a second one firing. When the publisher supplies its own `Data["eventId"]`, that id is authoritative and the two publishes already share it, so those modes update correctly even across a plugin restart.
|
|
233
|
+
|
|
234
|
+
The replacing backends therefore show **one** notification whose text improves in place. The message id is remembered in memory per tag for 15 minutes; after a plugin restart, or if the original message was deleted, the update is delivered as a new (quiet) message instead of being lost.
|
|
235
|
+
|
|
236
|
+
Only the `silent` follow-up replaces. Detection tags repeat across events (`motion:cam-1` is the same tag every time that camera sees something), so a *new* alert reusing a tag always posts a new message — your chat history is never rewritten by a later event.
|
|
237
|
+
|
|
238
|
+
**Critical alerts ignore `silent`** — a `critical` severity notification always alerts, per the SDK contract.
|
|
239
|
+
|
|
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.
|
|
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
|
+
|
|
211
300
|
## Configuring your target (v1: one active target)
|
|
212
301
|
|
|
213
302
|
There is no "add device" flow. Instead, configure the plugin itself:
|
|
@@ -215,7 +304,9 @@ There is no "add device" flow. Instead, configure the plugin itself:
|
|
|
215
304
|
1. Open the **Notify** plugin's page in camera.ui (Plugins → Notify).
|
|
216
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.
|
|
217
306
|
3. Fill in that service's fields — only the selected service's fields are shown; the rest are condition-gated out.
|
|
218
|
-
4.
|
|
307
|
+
4. Optionally set **Follow-up updates** (see [Follow-up updates](#follow-up-updates-ai-descriptions)) — defaults to delivering the AI description quietly.
|
|
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.
|
|
219
310
|
|
|
220
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).
|
|
221
312
|
|
|
@@ -318,9 +409,24 @@ dispatch logic in `notifier.go` all pick up the new backend automatically.
|
|
|
318
409
|
|
|
319
410
|
```bash
|
|
320
411
|
go test ./src/... # full suite
|
|
321
|
-
go test ./src/... -race -count=1 # race detector
|
|
412
|
+
go test ./src/... -race -count=1 # race detector (needs cgo: apt-get install gcc)
|
|
413
|
+
npm run lint # golangci-lint, configured by .golangci.yml
|
|
414
|
+
npm run format # gofmt + go fix
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Linting is golangci-lint only. It bundles staticcheck's analyzers (`SA`/`S`/`ST`/`QF`), so running
|
|
418
|
+
the standalone `staticcheck` binary alongside it only duplicates findings — and, being a separate
|
|
419
|
+
tool, it can't read the path-scoped exclusions in `.golangci.yml`.
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
|
322
423
|
```
|
|
323
424
|
|
|
425
|
+
`.golangci.yml` excludes three deliberate idioms, each scoped as narrowly as the linter allows so
|
|
426
|
+
the check stays live everywhere else: `defer resp.Body.Close()` (errcheck), and — in `_test.go`
|
|
427
|
+
only — passing a nil `Context` to exercise each backend's `ctx == nil` fallback (SA1012) and the
|
|
428
|
+
`!(a <= b && b <= c)` monotonicity assertions (QF1001).
|
|
429
|
+
|
|
324
430
|
## License
|
|
325
431
|
|
|
326
432
|
[MIT](./LICENSE.md).
|
package/bundle.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "Notify",
|
|
3
3
|
"name": "@calebcall/camera-ui-notify",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "A fully-local, multi-backend camera.ui notifier plugin: delivers notifications to ntfy, Gotify, or a generic webhook, entirely on your own hardware with no cloud dependency.",
|
|
4
|
+
"version": "0.9.0",
|
|
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",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"bundle:dev": "npm run build && cross-env MODE=development cui bundle",
|
|
13
13
|
"format": "gofmt -w ./src/* && go fix ./src/...",
|
|
14
14
|
"install-updates": "npm i --save --force && go mod tidy",
|
|
15
|
-
"lint": "
|
|
15
|
+
"lint": "golangci-lint run ./src/...",
|
|
16
16
|
"prepublishOnly": "node -e \"if(!process.env.SAFE_PUBLISH){console.error('Error: Please use @camera.ui/cli to publish the plugin:\\n npm run publish:alpha\\n npm run publish:beta\\n npm run publish:latest\\n');process.exit(1)}\"",
|
|
17
17
|
"publish:alpha": "npm i --save --force && npm run bundle && cui publish --alpha",
|
|
18
18
|
"publish:beta": "npm i --save --force && npm run bundle && cui publish --beta",
|
|
@@ -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",
|