@calebcall/camera-ui-notify 0.4.1 → 0.5.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
@@ -5,6 +5,30 @@ 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
+ ## [0.5.0] - 2026-07-25
9
+
10
+ ### Fixed
11
+
12
+ - **Images now delivered when the publisher uses `ImageURL`** — the official (closed) NVR publishes the
13
+ snapshot as a hosted `ImageURL` rather than inline `Thumbnail` bytes, so the inline-only backends
14
+ (Telegram, Discord, Pushover) delivered text with no image. `SendNotification` now fetches an
15
+ `ImageURL` once (when no inline `Thumbnail` is present) and attaches the bytes, so every backend
16
+ renders the image regardless of which NVR published it. A fetch failure degrades gracefully: the
17
+ `ImageURL` is left intact for URL-capable backends (ntfy/Gotify/webhook) and delivery is never
18
+ aborted. Fetches are capped at 8 MiB.
19
+
20
+ ### Added
21
+
22
+ - **Send-path logging** — `sendNotification` now logs the incoming notification (title, severity, which
23
+ image fields are present), image resolution, and per-device delivery outcome via `Log`/`Success`/
24
+ `Warn`/`Error` (visible without the debug flag), so the send flow is observable in normal operation.
25
+
26
+ ## [0.4.2] - 2026-07-25
27
+
28
+ ### Updated
29
+
30
+ - **README** - Updated README.md to fix broken lines and fix incorrect statements.
31
+
8
32
  ## [0.4.1] - 2026-07-25
9
33
 
10
34
  ### Changed
package/README.md CHANGED
@@ -10,56 +10,29 @@
10
10
 
11
11
  ## Why this exists
12
12
 
13
- camera.ui separates two notification roles:
14
13
 
15
- - **Notifiers** (interface `Notifier`) own delivery *devices* and actually deliver notifications.
16
- The host's `NotificationManager.notify()` fans every notification out to **all** running plugins
17
- implementing `Notifier`.
14
+ There is intentionally no notifier in the open ecosystem: the closed, official NVR bundles a notifier that pushed to the camera.ui mobile app through camera.ui's proprietary FCM/APNs cloud relay. This plugin takes the only fully-local road to real background push instead: delivering to a push/webhook service *you* run or control.
18
15
 
19
- There is intentionally no notifier in the open ecosystem: the closed, official NVR bundled a
20
- notifier that pushed to the camera.ui mobile app through camera.ui's proprietary FCM/APNs cloud
21
- relay. This plugin takes the only fully-local road to real background push instead: delivering to a push/webhook service *you* run or control.
22
-
23
- This plugin is a pure **notifier**. It is decoupled from any single publisher — it delivers
24
- notifications from **any** publisher, including our NVR plugin's detection events and camera.ui's
25
- own system notifications, not just one plugin's output.
16
+ This plugin is a pure **notifier**. It is decoupled from any single publisher — it delivers notifications from **any** publisher, including our NVR plugin's detection events and camera.ui's own system notifications, not just one plugin's output.
26
17
 
27
18
  ## How it works
28
19
 
29
20
  One plugin, contract `interfaces: [Notifier]`, built around a pluggable-backend (strategy) pattern:
30
21
 
31
- - A `Backend` interface abstracts one delivery service (id, label, config schema, target
32
- validation, and delivery).
33
- - A package-level registry maps each backend's stable id to its implementation; every backend
34
- self-registers from its own `init()`.
35
- - The plugin holds **one active target** in its own persisted config (`StorageSchema`): which
36
- service is selected, plus that service's validated fields. `getDevices` synthesizes a single
37
- `sdk.NotifierDevice` from this config on every call — there is no device registry.
38
- - `sendNotification` dispatches the synthesized device to its backend's `Send`. A backend failure
39
- is logged and returned to the host.
22
+ - A `Backend` interface abstracts one delivery service (id, label, config schema, target validation, and delivery).
23
+ - A package-level registry maps each backend's stable id to its implementation; every backend self-registers from its own `init()`.
24
+ - The plugin holds **one active target** in its own persisted config (`StorageSchema`): which service is selected, plus that service's validated fields. `getDevices` synthesizes a single `sdk.NotifierDevice` from this config on every call — there is no device registry.
25
+ - `sendNotification` dispatches the synthesized device to its backend's `Send`. A backend failure is logged and returned to the host.
40
26
 
41
27
  ### Why config, not device registration
42
28
 
43
- Earlier versions of this plugin modeled targets as registrable `NotifierDevice`s, the way
44
- camera.ui's own mobile-push notifier does. That model turned out to be unreachable from the stock
45
- UI: `registerDevice` is only ever called by the camera.ui mobile app's push-registration flow,
46
- which is hardcoded to the official NVR's plugin name. The generic notification settings panel
47
- renders `notificationSettings()` read-only — it has no "add a device" affordance for third-party
48
- plugins. The one part of the stock UI that **does** render an editable, savable form for a
49
- third-party plugin is its own settings page, which renders whatever `StorageSchema` the plugin
50
- declares. So Notify now models its target as plugin config instead of a device: you configure it
51
- once, in the plugin's own settings, and `getDevices` synthesizes the device the host's
52
- `Notifier` interface expects from that config.
53
-
54
- Adding a new backend later is **one new file** — `src/backend/<name>.go` implementing `Backend`
55
- plus `Register(...)` in its `init()` — and a version bump. No new plugin, no core change, no
56
- changes to `plugin.go`.
29
+ Earlier versions of this plugin modeled targets as registrable `NotifierDevice`s, the way camera.ui's own mobile-push notifier does. That model turned out to be unreachable from the stock UI: `registerDevice` is only ever called by the camera.ui mobile app's push-registration flow, which is hardcoded to the official NVR's plugin name. The generic notification settings panel renders `notificationSettings()` read-only — it has no "add a device" affordance for third-party plugins. The one part of the stock UI that **does** render an editable, savable form for a third-party plugin is its own settings page, which renders whatever `StorageSchema` the plugin declares. So Notify now models its target as plugin config instead of a device: you configure it once, in the plugin's own settings, and `getDevices` synthesizes the device the host's `Notifier` interface expects from that config.
30
+
31
+ Adding a new backend later is **one new file** — `src/backend/<name>.go` implementing `Backend` plus `Register(...)` in its `init()` — and a version bump. No new plugin, no core change, no changes to `plugin.go`.
57
32
 
58
33
  ## Backends (v1)
59
34
 
60
- Severity is mapped consistently across backends via `backend.PriorityScale`, which spreads
61
- camera.ui's four severity levels (`info` → `warn` → `error` → `critical`) evenly across each
62
- backend's native priority range, `info` at the low end and `critical` at the high end.
35
+ Severity is mapped consistently across backends via `backend.PriorityScale`, which spreads camera.ui's four severity levels (`info` → `warn` → `error` → `critical`) evenly across each backend's native priority range, `info` at the low end and `critical` at the high end.
63
36
 
64
37
  ### ntfy
65
38
 
@@ -71,9 +44,7 @@ Publishes to [ntfy.sh](https://ntfy.sh) or a self-hosted ntfy server.
71
44
  | `topic` | yes | — | The ntfy topic to publish to. |
72
45
  | `token` | no | — | Access token for a protected/self-hosted topic, sent as `Authorization: Bearer <token>`. |
73
46
 
74
- Delivery: `POST {server}/{topic}` with the notification body as the request body, plus `Title`,
75
- `Priority` (1–5, from severity), `Click` (deep link, if set), and `Attach`/`Icon` (image URL, if
76
- set) headers.
47
+ 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.
77
48
 
78
49
  ### Gotify
79
50
 
@@ -84,9 +55,7 @@ Publishes to a self-hosted [Gotify](https://gotify.net) server.
84
55
  | `server` | yes | Base URL of the Gotify server. Trailing `/` trimmed. |
85
56
  | `token` | yes | Gotify application token, used to authenticate published messages. |
86
57
 
87
- Delivery: `POST {server}/message?token={token}` with a JSON body `{title, message, priority}`
88
- (priority 0–10, from severity), plus a `client::notification.click` extra for the deep link and a
89
- `bigImageUrl` extra when an image URL is set.
58
+ 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.
90
59
 
91
60
  ### Generic webhook
92
61
 
@@ -99,9 +68,7 @@ Delivers to any HTTP endpoint you provide — the fallback for anything without
99
68
  | `headerName` | no | — | Optional custom header name (e.g. for a shared secret). Requires `headerValue` if set. |
100
69
  | `headerValue` | no | — | Value of the custom header. Requires `headerName` if set. |
101
70
 
102
- Delivery: `{method} {url}` with `Content-Type: application/json` and (if configured) the custom
103
- header, carrying a JSON body of `{title, subtitle, body, severity, tag, imageUrl, deepLink, data,
104
- createdAt, thumbnailBase64}`.
71
+ 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}`.
105
72
 
106
73
  ### Pushover
107
74
 
@@ -112,9 +79,7 @@ Hosted push to the [Pushover](https://pushover.net) app.
112
79
  | `token` | yes | Pushover application API token/key. |
113
80
  | `user` | yes | Your Pushover user or group key. |
114
81
 
115
- Delivery: `POST https://api.pushover.net/1/messages.json` with title/message and a priority
116
- (Info→0 normal, everything higher→1 high; never emergency). The snapshot **image** is sent as an
117
- `attachment`, and an absolute deep link becomes a supplementary `url`.
82
+ 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`.
118
83
 
119
84
  ### Telegram
120
85
 
@@ -125,8 +90,7 @@ Delivers to a chat via a [Telegram bot](https://core.telegram.org/bots).
125
90
  | `token` | yes | Bot token from @BotFather. |
126
91
  | `chat` | yes | Chat ID to deliver to. |
127
92
 
128
- Delivery: `sendPhoto` (with the snapshot **image** + caption) when a thumbnail is present, otherwise
129
- `sendMessage`. An absolute deep link is added as an inline "Open camera" button.
93
+ Delivery: `sendPhoto` (with the snapshot **image** + caption) when a thumbnail is present, otherwise `sendMessage`. An absolute deep link is added as an inline "Open camera" button.
130
94
 
131
95
  ### Discord
132
96
 
@@ -136,8 +100,7 @@ Delivers to a channel via a Discord [webhook](https://support.discord.com/hc/en-
136
100
  | --------- | -------- | --------------------------------------- |
137
101
  | `webhook` | yes | Channel webhook URL. |
138
102
 
139
- Delivery: a rich embed (title, body, severity color — blue/yellow/red) with the snapshot **image**
140
- attached; an absolute deep link makes the title a link.
103
+ 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.
141
104
 
142
105
  > **Images:** ntfy, Pushover, Telegram, and Discord all render the detection snapshot. Gotify is
143
106
  > text + link only (it needs a hosted image URL, which this fully-local plugin doesn't provide).
@@ -150,39 +113,28 @@ attached; an absolute deep link makes the title a link.
150
113
  There is no "add device" flow. Instead, configure the plugin itself:
151
114
 
152
115
  1. Open the **Notify** plugin's page in camera.ui (Plugins → Notify).
153
- 2. In its settings, pick a **Service** (`ntfy`, `Gotify`, `Generic webhook`, `Pushover`, `Telegram`,
154
- or `Discord`) from the dropdown built from the registered backends.
155
- 3. Fill in that service's fields only the selected service's fields are shown; the rest are
156
- condition-gated out.
157
- 4. Save. The config is validated (`ParseTarget`) the next time a notification is dispatched;
158
- `getDevices` then synthesizes one delivery target from it, and notifications from any publisher
159
- are delivered there.
160
-
161
- This is a **single, instance-wide target** in v1 — there's no way to register several devices at
162
- once. Changing the config replaces the previous target rather than adding to it. Delivery for that
163
- one target is a single request per notification (no fan-out to worry about, since there's only one
164
- device).
116
+ 2. In its settings, pick a **Service** (`ntfy`, `Gotify`, `Generic webhook`, `Pushover`, `Telegram`, or `Discord`) from the dropdown built from the registered backends.
117
+ 3. Fill in that service's fields — only the selected service's fields are shown; the rest are condition-gated out.
118
+ 4. 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.
119
+
120
+ 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).
165
121
 
166
122
  ## Tech stack
167
123
 
168
124
  - **Go** (matches the NVR plugin; same build/deploy toolchain).
169
125
  - **camera.ui SDK (Go)** for the plugin runtime, the `Notifier` RPC surface, and device storage.
170
- - Delivery via each backend's plain HTTP API using the Go stdlib `net/http` — no third-party HTTP
171
- client libraries.
172
- - Config persistence via the plugin's own `DeviceStorage`, holding the selected service and its
173
- fields as declared by `StorageSchema()` — no SQLite; it's a handful of scalar values.
126
+ - Delivery via each backend's plain HTTP API using the Go stdlib `net/http` — no third-party HTTP client libraries.
127
+ - Config persistence via the plugin's own `DeviceStorage`, holding the selected service and its fields as declared by `StorageSchema()` — no SQLite; it's a handful of scalar values.
174
128
 
175
129
  ## Prerequisites
176
130
 
177
131
  - **Go 1.26+** (to build the plugin binary).
178
- - **Node.js 22+** and the plugin's dev dependencies (`npm install`) to produce the `contract.cjs`
179
- bundle via the camera.ui CLI.
132
+ - **Node.js 22+** and the plugin's dev dependencies (`npm install`) to produce the `contract.cjs` bundle via the camera.ui CLI.
180
133
  - A running camera.ui instance you control (the host where the plugin gets installed).
181
134
 
182
135
  ## Build & deploy locally
183
136
 
184
- camera.ui loads the **built artifact**, not source — a `git pull` alone does nothing. You must
185
- build the bundle (`contract.cjs` + the platform binary) and copy it into the install slot.
137
+ camera.ui loads the **built artifact**, not source — a `git pull` alone does nothing. You must build the bundle (`contract.cjs` + the platform binary) and copy it into the install slot.
186
138
 
187
139
  The install slot is:
188
140
 
@@ -190,8 +142,7 @@ The install slot is:
190
142
  <camera.ui-install>/plugins/@calebcall/camera-ui-notify/
191
143
  ```
192
144
 
193
- and the plugin's config (the selected service and its fields) lives under
194
- `<camera.ui-install>/volume/plugins/storage/@calebcall/camera-ui-notify/` — reinstalling the code does not touch it.
145
+ and the plugin's config (the selected service and its fields) lives under `<camera.ui-install>/volume/plugins/storage/@calebcall/camera-ui-notify/` — reinstalling the code does not touch it.
195
146
 
196
147
  > **Unlike the NVR plugin**, there is no hardcoded-package-id requirement here: the camera.ui host
197
148
  > discovers notifiers by the `Notifier` **interface** declared in the plugin's contract, not by a
@@ -214,10 +165,7 @@ npm run bundle:dev
214
165
  D=<camera.ui-install>/plugins/@calebcall/camera-ui-notify
215
166
  mkdir -p "$D" && rm -rf "$D"/* && cp -a bundle/. "$D/"
216
167
 
217
- # 4. First time only: enable it — remove the "@calebcall/camera-ui-notify" line
218
- # from disabledPlugins in <camera.ui-install>/volume/camera.ui.yaml
219
-
220
- # 5. Restart camera.ui
168
+ # 4. Restart camera.ui
221
169
  systemctl restart cameraui # or however your instance is managed
222
170
  ```
223
171
 
@@ -233,8 +181,7 @@ npm install
233
181
  npm run bundle:dev
234
182
 
235
183
  # 2. Cross-compile the Linux binary into the bundle's dev path
236
- GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
237
- go build -ldflags "-s -w" -o bundle/dist/bin/plugin ./src/
184
+ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \ go build -ldflags "-s -w" -o bundle/dist/bin/plugin ./src/
238
185
  chmod 755 bundle/dist/bin/plugin
239
186
 
240
187
  # 3. Ship it (strip macOS junk so the server dir stays clean)
@@ -242,15 +189,10 @@ COPYFILE_DISABLE=1 tar czf /tmp/notify-install.tgz -C bundle .
242
189
  scp /tmp/notify-install.tgz root@YOUR_SERVER:/tmp/
243
190
 
244
191
  # 4. On the server: install into the slot, enable (first time), restart
245
- ssh root@YOUR_SERVER '
246
- D=<camera.ui-install>/plugins/@calebcall/camera-ui-notify
247
- mkdir -p "$D" && rm -rf "$D"/* && tar xzf /tmp/notify-install.tgz -C "$D"
248
- systemctl restart cameraui
249
- '
192
+ ssh root@YOUR_SERVER 'D=<camera.ui-install>/plugins/@calebcall/camera-ui-notify mkdir -p "$D" && rm -rf "$D"/* && tar xzf /tmp/notify-install.tgz -C "$D"; systemctl restart cameraui'
250
193
  ```
251
194
 
252
- Set `GOARCH=arm64` for ARM hosts. The binary is resolved from the dev path
253
- `<slot>/dist/bin/plugin` before any platform `node_modules` package, so no npm publish is involved.
195
+ Set `GOARCH=arm64` for ARM hosts. The binary is resolved from the dev path `<slot>/dist/bin/plugin` before any platform `node_modules` package, so no npm publish is involved.
254
196
 
255
197
  ### Verify it loaded and is being used
256
198
 
@@ -260,16 +202,12 @@ Tail the camera.ui log after restart:
260
202
  grep -iE "Notify|Spawning Go|notify: rpc" <camera.ui-install>/volume/camera.ui.log | tail
261
203
  ```
262
204
 
263
- You want to see the plugin spawn (`Spawning Go plugin ... dist/bin/plugin`) **and** the host
264
- actually calling into it — e.g. `notify: rpc getDevices` / `notificationSettings` /
265
- `sendNotification`.
205
+ You want to see the plugin spawn (`Spawning Go plugin ... dist/bin/plugin`) **and** the host actually calling into it — e.g. `notify: rpc getDevices` / `notificationSettings` / `sendNotification`.
266
206
 
267
207
  ## Adding a new backend
268
208
 
269
- 1. Create `src/backend/<name>.go` implementing the `Backend` interface (`ID`, `Label`, `Schema`,
270
- `ParseTarget`, `Send`) see `ntfy.go`, `gotify.go`, or `webhook.go` for the shape.
271
- 2. Gate every schema field with `Condition: []sdk.SchemaCondition{{Key: "service", Value: ID()}}`
272
- so it only renders when the new backend is selected.
209
+ 1. Create `src/backend/<name>.go` implementing the `Backend` interface (`ID`, `Label`, `Schema`, `ParseTarget`, `Send`) — see `ntfy.go`, `gotify.go`, or `webhook.go` for the shape.
210
+ 2. Gate every schema field with `Condition: []sdk.SchemaCondition{{Key: "service", Value: ID()}}` so it only renders when the new backend is selected.
273
211
  3. Call `backend.Register(new<Name>())` from an `init()` function in the same file.
274
212
  4. Bump the version in `package.json`.
275
213
 
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.1",
4
+ "version": "0.5.0",
5
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.",
6
6
  "author": "calebcall (https://github.com/calebcall/camera-ui-notify)",
7
7
  "main": "./main.go",
@@ -45,14 +45,14 @@
45
45
  "os": [],
46
46
  "cpu": [],
47
47
  "optionalDependencies": {
48
- "@calebcall/camera-ui-notify-darwin-arm64": "0.4.1",
49
- "@calebcall/camera-ui-notify-darwin-amd64": "0.4.1",
50
- "@calebcall/camera-ui-notify-linux-amd64": "0.4.1",
51
- "@calebcall/camera-ui-notify-linux-arm64": "0.4.1",
52
- "@calebcall/camera-ui-notify-windows-amd64": "0.4.1",
53
- "@calebcall/camera-ui-notify-windows-arm64": "0.4.1",
54
- "@calebcall/camera-ui-notify-linux-amd64-musl": "0.4.1",
55
- "@calebcall/camera-ui-notify-linux-arm64-musl": "0.4.1"
48
+ "@calebcall/camera-ui-notify-darwin-arm64": "0.5.0",
49
+ "@calebcall/camera-ui-notify-darwin-amd64": "0.5.0",
50
+ "@calebcall/camera-ui-notify-linux-amd64": "0.5.0",
51
+ "@calebcall/camera-ui-notify-linux-arm64": "0.5.0",
52
+ "@calebcall/camera-ui-notify-windows-amd64": "0.5.0",
53
+ "@calebcall/camera-ui-notify-windows-arm64": "0.5.0",
54
+ "@calebcall/camera-ui-notify-linux-amd64-musl": "0.5.0",
55
+ "@calebcall/camera-ui-notify-linux-arm64-musl": "0.5.0"
56
56
  },
57
57
  "files": [
58
58
  "bundle.zip",