@calebcall/camera-ui-notify 0.4.0 → 0.4.2

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